package crmdna.client.program;

import static crmdna.common.OfyService.ofy;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import com.google.appengine.api.memcache.MemcacheServiceFactory;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.Ref;
import com.googlecode.objectify.cmd.Query;

import crmdna.client.group.Group;
import crmdna.client.group.Group.GroupProp;
import crmdna.client.group.GroupEntity;
import crmdna.client.programtype.ProgramType;
import crmdna.client.programtype.ProgramType.ProgramTypeProp;
import crmdna.client.programtype.ProgramTypeEntity;
import crmdna.client.sequence.Sequence;
import crmdna.client.sequence.Sequence.SequenceType;
import crmdna.client.teacher.Teacher;
import crmdna.client.teacher.Teacher.TeacherProp;
import crmdna.client.teacher.TeacherEntity;
import crmdna.client.user.User;
import crmdna.client.user.User.Action;
import crmdna.client.user.User.ResourceType;
import crmdna.client.venue.Venue;
import crmdna.client.venue.Venue.VenueProp;
import crmdna.client.venue.VenueEntity;
import crmdna.common.APIException;
import crmdna.common.APIResponse.Status;
import crmdna.common.Utils;
import crmdna.crmdna.Client;

public class Program {	
	public static class ProgramProp implements Comparable<ProgramProp>{
		public long programId;
		public ProgramTypeProp programTypeProp;
		public GroupProp groupProp;
		public VenueProp venueProp;
		public TeacherProp teacherProp;
		public int startYYYYMMDD;
		public int endYYYYMMDD;
		public int numBatches;
		public String description;				
		
		//dependent
		public String name;
		
		public void populateName() {
			name = programTypeProp.displayName + " " 
					+ Utils.getDurationAsString(startYYYYMMDD, endYYYYMMDD)  + " @ "				
					+ venueProp.displayName 
					+ " (" + groupProp.displayName + ")";	
		}
		
		@Override
		public int compareTo(ProgramProp arg0) {
			//sort order is: -startYYYYMMDD, +programtypename, +group, +venue
									
			if (startYYYYMMDD != arg0.startYYYYMMDD) {
				Long start = new Long(startYYYYMMDD);
				Long arg0Start = new Long(arg0.startYYYYMMDD);
				return arg0Start.compareTo(start);
			}			
			
			if (! programTypeProp.name.equals(arg0.programTypeProp.name))
				return programTypeProp.name.compareTo(arg0.programTypeProp.name);
			
			if (! groupProp.name.equals(arg0.groupProp.name))
				return groupProp.name.compareTo(arg0.groupProp.name);
			
			return venueProp.name.compareTo(arg0.venueProp.name);					
		}
		
		public List<SessionProp> getSessions(long dateYYYYMMDD) {
			List<SessionProp> sessionProps = new ArrayList<>();
			
			for (int batchIndex = 0; batchIndex < numBatches; batchIndex++) {
				SessionProp sessionProp = new SessionProp();
				sessionProp.dateYYYYMMDD = dateYYYYMMDD;
				sessionProp.programId = programId;
				sessionProp.batchNo = batchIndex + 1;
				sessionProp.programType = programTypeProp.displayName;
				sessionProp.center = groupProp.displayName;
				sessionProp.venue = venueProp.displayName;
				
				sessionProp.populateTitle(numBatches);								
				
				sessionProps.add(sessionProp);
			}
			
			return sessionProps;
		}
		
		public int getNumSessions() {
			//TODO: this is buggy, fix it
			return endYYYYMMDD - startYYYYMMDD + 1;
		}
		
		public void ensureValidSessionDate(long sessionDateYYYYMMDD) {
			Utils.ensureFormatYYYYMMDD(sessionDateYYYYMMDD);
			
			if ((sessionDateYYYYMMDD < startYYYYMMDD) || (sessionDateYYYYMMDD > endYYYYMMDD))
				Utils.throwIncorrectSpecException("Session date [" + sessionDateYYYYMMDD + 
						"] should be between start [" + startYYYYMMDD + 
						"] and end date [" + endYYYYMMDD + "] for Program [" + name + "]");
		}
	}
	
	public static class SessionProp implements Comparable<SessionProp> {
		public long dateYYYYMMDD;
		public long programId;
		public String programType;
		public String venue;
		public String center;
		public int batchNo;
		public String title;
		
		@Override
		public int compareTo(SessionProp o) {			
			return this.title.compareTo(o.title);
		}
		
		public void populateTitle(long numBatches) {
			String title = Utils.toDDMMM(dateYYYYMMDD) + " ";
			if (numBatches > 1)
				title += "B" + batchNo + " ";
			
			if (programType.length() > 11)
				title += programType.substring(0, 10) + "~" + " ";
			else {
				title += programType + " ";
			}
			
			if (venue.length() > 11)
				title += "@ " + venue.substring(0, 10) + "~";
			else
				title += "@ " + venue;
			
			this.title = title;
		}
	}
		
	public static ProgramProp create(String client, long groupId, long programTypeId,  
			long venueId, long teacherId, int startYYYYMMDD, int endYYYYMMDD, int numBatches,
			String description, String login) {
		
		Client.ensureValidClient(client);
		User.ensureAccess(client, login, ResourceType.PROGRAM, groupId, Action.WRITE);
		
		GroupEntity groupEntity = Group.safeGet(client, groupId);
		ProgramTypeEntity programTypeEntity = ProgramType.safeGet(client, programTypeId);
		VenueEntity venueEntity = Venue.safeGet(client, venueId);
		TeacherEntity teacherEntity = Teacher.safeGet(client, teacherId);
		
		ProgramEntity programEntity = new ProgramEntity();				
		programEntity.programTypeRef = Ref.create(programTypeEntity);
		programEntity.venueRef = Ref.create(venueEntity);
		programEntity.groupRef = Ref.create(groupEntity);
		programEntity.teacherRef = Ref.create(teacherEntity);
		programEntity.startYYYYMMDD = startYYYYMMDD;
		programEntity.endYYYYMMDD = endYYYYMMDD;
		programEntity.numBatches = numBatches;
		programEntity.description = description;
		
		ensureValid(programEntity);
		
		ensureNotPresentInDB(client, programEntity);		
		safeAddToMemcache(client, programEntity);
								
		programEntity.programId = Sequence.getNext(client, SequenceType.PROGRAM);					
		
		ofy(client).save().entity(programEntity).now();		
		return programEntity.toProp();		
	}
	
	public static Map<Long, ProgramProp> get(String client, Iterable<Long> programIds) {
		
		Client.ensureValidClient(client);
		
		Map<Long, ProgramEntity> programEntities = ofy(client).load().type(ProgramEntity.class).ids(programIds);
		
		Map<Long, ProgramProp> programProps = new HashMap<>();
		
		for (Long programId : programEntities.keySet()) {
			ProgramEntity programEntity = programEntities.get(programId);
			
			ProgramProp programProp = null;
			if (programEntity != null)
				programProp = programEntity.toProp();
			
			programProps.put(programId, programProp);
		}
		
		return programProps;			
	}
	
	public static ProgramProp update(String client, long programId, Long newVenueId, 
			Long newTeacherId, Integer newStartYYYYMMDD, Integer newEndYYYYMMDD, 
			Integer newNumBatches, String newDescription, String login) {
		
		//warning: this method will modify session cache if an exception is thrown
		
		Client.ensureValidClient(client);
		ProgramEntity programEntity = safeGet(client, programId);
				
		User.ensureAccess(client, login, ResourceType.PROGRAM, 
				programEntity.groupRef.get().toProp().groupId, Action.WRITE);				
		
		boolean checkUnique = false;				
		if (null != newVenueId) {
			VenueEntity venueEntity = Venue.safeGet(client, newVenueId);
			programEntity.venueRef = Ref.create(venueEntity);
			checkUnique = true;
		}
		
		if (null != newTeacherId) {
			TeacherEntity teacherEntity = Teacher.safeGet(client, newTeacherId);
			programEntity.teacherRef = Ref.create(teacherEntity);
		}
		
		if (null != newStartYYYYMMDD) {
			Utils.ensureFormatYYYYMMDD(newStartYYYYMMDD);
			programEntity.startYYYYMMDD = newStartYYYYMMDD;
			checkUnique = true;
		}
		
		if (null != newEndYYYYMMDD) {
			Utils.ensureFormatYYYYMMDD(newEndYYYYMMDD);
			programEntity.endYYYYMMDD = newEndYYYYMMDD;
			checkUnique = true;
		}
		
		if (null != newNumBatches) {
			programEntity.numBatches = newNumBatches;
			checkUnique = true;
		}
		
		if (null != newDescription)
			programEntity.description = newDescription;
			
		ensureValid(programEntity);
		
		if (checkUnique) {
			ensureNotPresentInDB(client, programEntity);
			safeAddToMemcache(client, programEntity);
		}				
						
		ofy(client).save().entity(programEntity).now();		
		return programEntity.toProp();		
	}
	
	private static void ensureValid(ProgramEntity programEntity) {
		Utils.ensureFormatYYYYMMDD(programEntity.startYYYYMMDD);
		Utils.ensureFormatYYYYMMDD(programEntity.endYYYYMMDD);
		
		if (programEntity.startYYYYMMDD > programEntity.endYYYYMMDD)
			throw new APIException(Status.ERROR_RESOURCE_INCORRECTLY_SPECIFIED, 
					"Start date [" + programEntity.startYYYYMMDD + "] is greater than end date [" +
							programEntity.endYYYYMMDD + "]");
		
		if (programEntity.numBatches < 1 )
			throw new APIException(Status.ERROR_RESOURCE_INCORRECTLY_SPECIFIED, 
					"Invalid number of batches [" + programEntity.numBatches + 
					"]. numBatches should be positive");
		
		if ((programEntity.programTypeRef == null) ||
			(programEntity.venueRef == null) ||
			(programEntity.teacherRef == null) ||
			(programEntity.groupRef == null))			
				throw new APIException(Status.ERROR_RESOURCE_INCORRECTLY_SPECIFIED, 
					"Either program type or venue or teacher or group not populated");
	}
	
	public static ProgramEntity safeGet(String client, long programId) {
		Client.ensureValidClient(client);
		
		ProgramEntity entity = ofy(client).load().type(ProgramEntity.class).id(programId).get();
		if (null == entity)
			throw new APIException(Status.ERROR_RESOURCE_DOES_NOT_EXIST, 
					"There is no program with id [" + programId + "]");
		
		return entity;
	}	
	
	public static List<ProgramProp> query(String client, Long startYYYYMMDD, 
			Long endYYYYMMDD, Long programTypeId, 
			Set<Long> groupIds, Long venueId, Integer maxResultSize) {
		Client.ensureValidClient(client);
		
		Query<ProgramEntity> q = ofy(client).load().type(ProgramEntity.class);
		
		if (null != startYYYYMMDD)
			Utils.ensureFormatYYYYMMDD(startYYYYMMDD);
		
		if (null != endYYYYMMDD)
			Utils.ensureFormatYYYYMMDD(endYYYYMMDD);
						
		if (null != programTypeId) {
			ProgramTypeEntity programTypeEntity = ProgramType.safeGet(client, programTypeId);
			q = q.filter("programTypeRef", programTypeEntity);
		}
		
		if ((null != groupIds) && (groupIds.size() != 0)){							
			List<Key<GroupEntity>> groupKeys = new ArrayList<>();
			for (Long groupId : groupIds) {
				Key<GroupEntity> groupKey = Key.create(GroupEntity.class, groupId);
				groupKeys.add(groupKey);
			}
			
			q = q.filter("groupRef in", groupKeys);
		}
		
		if (null != venueId) {
			VenueEntity venueEntity = Venue.safeGet(client, venueId);
			q = q.filter("venueRef", venueEntity);
		}		
		
		//don't limit the query as this will mess up the sort order. 
		//get all results, sort and then limit the data that is returned
				
		List<ProgramEntity> entities = q.list();
						
		List<ProgramProp> props = new ArrayList<>();
		for (ProgramEntity entity : entities) {
			//discard entities that are after end date (if specified) 				
			if ((null != endYYYYMMDD) && (entity.endYYYYMMDD > endYYYYMMDD))
				continue;
			
			if ((null != startYYYYMMDD) && (entity.startYYYYMMDD < startYYYYMMDD))
				continue;
				
			props.add(entity.toProp());
		}
		
		Collections.sort(props);
		
		if ((maxResultSize != null) && (props.size() > maxResultSize))
			props = props.subList(0, maxResultSize);
		
		return props;
	}
	
	static List<ProgramProp> getOngoingPrograms(String client, long dateYYYYMMDD, String login) {
		Client.ensureValidClient(client);
		User.ensureValidUser(client, login);
		
		Utils.ensureFormatYYYYMMDD(dateYYYYMMDD);
		
		Set<Long> groupIds = User.getResources(client, login, ResourceType.GROUP, Action.WRITE);
		
		//remove groupId 0 as it is not a valid group
		groupIds.remove((long)0); 
		
		//if user does not have write access to any group just return
		if (0 == groupIds.size())
			return new ArrayList<>();					
		
		List<ProgramProp> programProps = query(client, null, null, null, groupIds, null, null);
		
		List<ProgramProp> ongoing = new ArrayList<>();
		for (ProgramProp programProp : programProps) {
			if ((dateYYYYMMDD >= programProp.startYYYYMMDD) && 
					(dateYYYYMMDD <= programProp.endYYYYMMDD))
				ongoing.add(programProp);
		}
		
		return ongoing;				
	}
	
	public static List<SessionProp> getOngoingSessions(String client, int dateYYYYMMDD, String login) {
		List<ProgramProp> programProps = getOngoingPrograms(client, dateYYYYMMDD, login);
		
		List<SessionProp> sessionProps = new ArrayList<>();
		for (ProgramProp programProp : programProps) {
			sessionProps.addAll(programProp.getSessions(dateYYYYMMDD));								
		}
		
		return sessionProps;
	}
	
					
	private static String getUniqueKey(String namespace, ProgramEntity programEntity) {
		
		return namespace + "_" + programEntity.programTypeRef.get().toProp().programTypeId + 
				"_" + programEntity.venueRef.get().toProp().venueId +
				"_" + programEntity.startYYYYMMDD + "_" + programEntity.endYYYYMMDD;
 	}
	
	private static void ensureNotPresentInDB(String namespace, ProgramEntity programEntity) {
		List<Key<ProgramEntity>> keys = ofy(namespace).load()
				.type(ProgramEntity.class).filter("programTypeRef", programEntity.programTypeRef)
				.filter("venueRef", programEntity.venueRef)
				.filter("startDateYYYMMDD", programEntity.startYYYYMMDD)
				.filter("endDateYYYYMMDD", programEntity.endYYYYMMDD).keys().list();
		
		if (keys.size() != 0)
			throw new APIException(Status.ERROR_RESOURCE_ALREADY_EXISTS, 
					"There is already a program with the same start date, end date, venue, program type and batch.");
	}
	
	private static void safeAddToMemcache(String namespace, ProgramEntity programEntity) {
		String key = getUniqueKey(namespace, programEntity);
		
		long val = MemcacheServiceFactory.getMemcacheService()
				.increment(key, 1, (long)0);
		
		if (val != 1)
			throw new APIException(Status.ERROR_RESOURCE_ALREADY_EXISTS, 
					"There is already a program with the same start date, end date, venue, program type and batch. ");
	}
}
